// Copyright 2022 Proyectos y Sistemas de Mantenimiento SL (eProsima).
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// ------------------------------------------------------------------
// Modification history:
// feature: provide query interfaces
// feature: discovery support client and server
// ------------------------------------------------------------------

#include <edds/rtps/attributes/ServerAttributes.h>
#include <deps/common/SystemInfo.hpp>
#include <ertps/utils/LocatorAlias.h>

#include <forward_list>

namespace evbs {
namespace edds {
namespace rtps {

template <>
const char* server_ostream_separators<char>::list_separator = "; ";
template <>
const wchar_t* server_ostream_separators<wchar_t>::list_separator = L"; ";
template <>
const char* server_ostream_separators<char>::locator_separator = "|";
template <>
const wchar_t* server_ostream_separators<wchar_t>::locator_separator = L"|";
const std::string& vbs_discovery_server_env() {
    static std::string servers;
    vbs::SystemInfo::get_env(DEFAULT_VBS_MASTER_URI, servers);
    return servers;
}
bool load_environment_server_info(ertps::rtps::LocatorList_t& servers_list) {
    return load_environment_server_info(vbs_discovery_server_env(), servers_list);
}

bool load_environment_server_info(const std::string& list, ertps::rtps::LocatorList_t& servers_list) {
    servers_list.clear();
    if (list.empty()) {
        return true;
    }

    /* Parsing ancillary regex
     * Addresses should be ; separated. IPLocator functions are used to identify them in the order:
     * IPv4, IPv6 or try dns resolution.
     **/
    const static std::regex VBS_SERVER_LIST_PATTERN(R"(([^;]*);?)");  //LCOV_EXCL_START
    const static std::regex VBS_IPV4_ADDRESSPORT_PATTERN(R"(^((?:[0-9]{1,3}\.){3}[0-9]{1,3})?:?(?:(\d+))?$)");
    const static std::regex VBS_IPV6_ADDRESSPORT_PATTERN(
        R"(^\[?((?:[0-9a-fA-F]{0,4}\:){0,7}[0-9a-fA-F]{0,4})?(?:\])?:?(?:(\d+))?$)");
    // Regex to handle DNS and UDPv4/6 expressions
    const static std::regex VBS_DNS_DOMAINPORT_PATTERN(R"(^(UDPv[46]?:\[[\w\.:-]{0,63}\]|[\w\.-]{0,63}):?(?:(\d+))?$)");
    // Regex to handle TCPv4/6 expressions
    const static std::regex VBS_DNS_DOMAINPORT_PATTERN_TCP(R"(^(TCPv[46]?:\[[\w\.:-]{0,63}\]):?(?:(\d+))?$)");

    // Filling port info
    auto process_port = [](int port, ertps::rtps::Locator_t& server) {
        if (port > std::numeric_limits<uint16_t>::max()) {
            throw std::out_of_range("Too large port passed into the server's list");
        }

        if (!ertps::rtps::IPLocator::setPhysicalPort(server, static_cast<uint16_t>(port))) {
            std::stringstream ss;
            ss << "Wrong port passed into the server's list " << port;
            throw std::invalid_argument(ss.str());
        }
    };

    // Add new server
    auto add_server2qos = [](std::forward_list<Locator>&& locators, ertps::rtps::LocatorList_t& servers_list) {
        for (auto& server_address : locators) {
            servers_list.push_back(std::move(server_address));
        }
    };

    try {
        // Do the parsing and populate the list
        ertps::rtps::Locator_t server_locator(LOCATOR_KIND_UDPv4, DEFAULT_VBS_SERVER_PORT);

        std::sregex_iterator server_it(list.begin(), list.end(), VBS_SERVER_LIST_PATTERN,
                                       std::regex_constants::match_not_null);

        while (server_it != std::sregex_iterator()) {
            // Retrieve the address (IPv4, IPv6 or DNS name)
            auto it = server_it->cbegin();
            auto end = server_it->cend();
            if (it == end && std::next(it) == end) {
                std::stringstream ss;
                ss << "Wrong server_it";
                throw std::invalid_argument(ss.str());
            }
            const std::smatch::value_type sm = *++(server_it->cbegin());

            if (sm.matched) {
                // Now we must parse the inner expression
                std::smatch mr;
                std::string locator(sm);

                if (locator.empty()) {
                    // It's intentionally empty to hint us to ignore this server
                }
                // Try first with IPv4
                else if (std::regex_match(locator, mr, VBS_IPV4_ADDRESSPORT_PATTERN,
                                          std::regex_constants::match_not_null)) {
                    std::smatch::iterator it = mr.cbegin();

                    // Traverse submatches
                    if (++it != mr.cend()) {
                        std::string address = it->str();
                        server_locator.kind = LOCATOR_KIND_UDPv4;
                        server_locator.set_Invalid_Address();

                        if (!ertps::rtps::IPLocator::setIPv4(server_locator, address)) {
                            std::stringstream ss;
                            ss << "Wrong ipv4 address passed into the server's list " << address;
                            throw std::invalid_argument(ss.str());
                        }

                        if (ertps::rtps::IPLocator::isAny(server_locator)) {
                            // A server cannot be reach in all interfaces, it's clearly a localhost call
                            ertps::rtps::IPLocator::setIPv4(server_locator, "127.0.0.1");
                        }

                        // Get port if any
                        int port = DEFAULT_VBS_SERVER_PORT;
                        if (++it != mr.cend() && it->matched) {
                            port = stoi(it->str());
                        }

                        process_port(port, server_locator);
                    }

                    // Add server to the list
                    add_server2qos(std::forward_list<Locator> {server_locator}, servers_list);
                }
                // Try IPv6 next
                else if (std::regex_match(locator, mr, VBS_IPV6_ADDRESSPORT_PATTERN,
                                          std::regex_constants::match_not_null)) {
                    std::smatch::iterator it = mr.cbegin();

                    // Traverse submatches
                    if (++it != mr.cend()) {
                        std::string address = it->str();
                        server_locator.kind = LOCATOR_KIND_UDPv6;
                        server_locator.set_Invalid_Address();

                        if (!ertps::rtps::IPLocator::setIPv6(server_locator, address)) {
                            std::stringstream ss;
                            ss << "Wrong ipv6 address passed into the server's list " << address;
                            throw std::invalid_argument(ss.str());
                        }

                        if (ertps::rtps::IPLocator::isAny(server_locator)) {
                            // A server cannot be reach in all interfaces, it's clearly a localhost call
                            ertps::rtps::IPLocator::setIPv6(server_locator, "::1");
                        }

                        // Get port if any
                        int port = DEFAULT_VBS_SERVER_PORT;
                        if (++it != mr.cend() && it->matched) {
                            port = stoi(it->str());
                        }

                        process_port(port, server_locator);
                    }

                    // Add server to the list
                    add_server2qos(std::forward_list<Locator> {server_locator}, servers_list);
                }
                // Try to resolve DNS
                else if (std::regex_match(locator, mr, VBS_DNS_DOMAINPORT_PATTERN,
                                          std::regex_constants::match_not_null)) {
                    std::forward_list<Locator> flist;

                    {
                        std::stringstream new_locator(locator,
                                                      std::ios_base::in | std::ios_base::out | std::ios_base::ate);

                        // First try the formal notation, add default port if necessary
                        if (!mr[2].matched) {
                            new_locator << ":" << DEFAULT_VBS_SERVER_PORT;
                        }

                        new_locator >> server_locator;
                    }

                    // Otherwise add all resolved locators
                    switch (server_locator.kind) {
                        case LOCATOR_KIND_UDPv4:
                        case LOCATOR_KIND_UDPv6:
                            flist.push_front(server_locator);
                            break;
                        case LOCATOR_KIND_INVALID: {
                            std::smatch::iterator it = mr.cbegin();

                            // Traverse submatches
                            if (++it != mr.cend()) {
                                std::string domain_name = it->str();
                                std::set<std::string> ipv4, ipv6;
                                std::tie(ipv4, ipv6) = ertps::rtps::IPLocator::resolveNameDNS(domain_name);

                                // Get port if any
                                int port = DEFAULT_VBS_SERVER_PORT;
                                if (++it != mr.cend() && it->matched) {
                                    port = stoi(it->str());
                                }

                                for (const std::string& loc : ipv4) {
                                    server_locator.kind = LOCATOR_KIND_UDPv4;
                                    server_locator.set_Invalid_Address();
                                    ertps::rtps::IPLocator::setIPv4(server_locator, loc);

                                    if (ertps::rtps::IPLocator::isAny(server_locator)) {
                                        // A server cannot be reach in all interfaces, it's clearly a localhost call
                                        ertps::rtps::IPLocator::setIPv4(server_locator, "127.0.0.1");
                                    }

                                    process_port(port, server_locator);
                                    flist.push_front(server_locator);
                                }

                                for (const std::string& loc : ipv6) {
                                    server_locator.kind = LOCATOR_KIND_UDPv6;
                                    server_locator.set_Invalid_Address();
                                    ertps::rtps::IPLocator::setIPv6(server_locator, loc);

                                    if (ertps::rtps::IPLocator::isAny(server_locator)) {
                                        // A server cannot be reach in all interfaces, it's clearly a localhost call
                                        ertps::rtps::IPLocator::setIPv6(server_locator, "::1");
                                    }

                                    process_port(port, server_locator);
                                    flist.push_front(server_locator);
                                }
                            }
                        }
                    }

                    if (flist.empty()) {
                        std::stringstream ss;
                        ss << "Wrong domain name passed into the server's list " << locator;
                        throw std::invalid_argument(ss.str());
                    }

                    // Add server to the list
                    add_server2qos(std::move(flist), servers_list);
                }
                // Try to resolve TCP DNS
                else if (std::regex_match(locator, mr, VBS_DNS_DOMAINPORT_PATTERN_TCP,
                                          std::regex_constants::match_not_null)) {
                    std::forward_list<Locator> flist;

                    {
                        std::stringstream new_locator(locator,
                                                      std::ios_base::in | std::ios_base::out | std::ios_base::ate);

                        // First try the formal notation, add default port if necessary
                        if (!mr[2].matched) {
                            new_locator << ":" << DEFAULT_VBS_TCP_SERVER_PORT;
                        }

                        new_locator >> server_locator;
                    }

                    // Otherwise add all resolved locators
                    switch (server_locator.kind) {
                        case LOCATOR_KIND_TCPv4:
                        case LOCATOR_KIND_TCPv6:
                            ertps::rtps::IPLocator::setLogicalPort(
                                server_locator, ertps::rtps::IPLocator::getPhysicalPort(server_locator));
                            flist.push_front(server_locator);
                            break;
                        case LOCATOR_KIND_INVALID: {
                            std::smatch::iterator it = mr.cbegin();

                            // Traverse submatches
                            if (++it != mr.cend()) {
                                std::string domain_name = it->str();
                                std::set<std::string> ipv4, ipv6;
                                std::tie(ipv4, ipv6) = ertps::rtps::IPLocator::resolveNameDNS(domain_name);

                                // Get port if any
                                int port = DEFAULT_VBS_TCP_SERVER_PORT;
                                if (++it != mr.cend() && it->matched) {
                                    port = stoi(it->str());
                                }

                                for (const std::string& loc : ipv4) {
                                    server_locator.kind = LOCATOR_KIND_TCPv4;
                                    server_locator.set_Invalid_Address();
                                    ertps::rtps::IPLocator::setIPv4(server_locator, loc);

                                    if (ertps::rtps::IPLocator::isAny(server_locator)) {
                                        // A server cannot be reach in all interfaces, it's clearly a localhost call
                                        ertps::rtps::IPLocator::setIPv4(server_locator, "127.0.0.1");
                                    }

                                    process_port(port, server_locator);
                                    ertps::rtps::IPLocator::setLogicalPort(server_locator, static_cast<uint16_t>(port));
                                    flist.push_front(server_locator);
                                }

                                for (const std::string& loc : ipv6) {
                                    server_locator.kind = LOCATOR_KIND_TCPv6;
                                    server_locator.set_Invalid_Address();
                                    ertps::rtps::IPLocator::setIPv6(server_locator, loc);

                                    if (ertps::rtps::IPLocator::isAny(server_locator)) {
                                        // A server cannot be reach in all interfaces, it's clearly a localhost call
                                        ertps::rtps::IPLocator::setIPv6(server_locator, "::1");
                                    }

                                    process_port(port, server_locator);
                                    ertps::rtps::IPLocator::setLogicalPort(server_locator, static_cast<uint16_t>(port));
                                    flist.push_front(server_locator);
                                }
                            }
                        }
                    }

                    if (flist.empty()) {
                        std::stringstream ss;
                        ss << "Wrong domain name passed into the server's list " << locator;
                        throw std::invalid_argument(ss.str());
                    }

                    // Add server to the list
                    add_server2qos(std::move(flist), servers_list);
                } else {
                    std::stringstream ss;
                    ss << "Wrong locator passed into the server's list " << locator;
                    throw std::invalid_argument(ss.str());
                }
            }

            // Advance to the next server if any
            ++server_it;
        }

        // Check for server info
        if (servers_list.empty()) {
            throw std::invalid_argument("No default server locators were provided.");
        }
    } catch (std::exception& e) {
        elogError(SERVER_CLIENT_DISCOVERY, RetCode_t::RETCODE_ERROR, e.what());
        servers_list.clear();
        return false;
    }

    return true;
}  // LCOV_EXCL_STOP
} /* namespace rtps */
} /* namespace edds */
} /* namespace evbs */
